//@author A0100965J
package memory;

import gui.MainConsole;
import hirondelle.date4j.DateTime;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;

import common.AttributeType;
import common.DateHelper;
import common.Task;
import common.TaskStatus;
import common.TaskType;

/**
 * This class provide all services to interact with the database
 */
public class Database {
	/**
	 * Singleton instance
	 */
	private static Database _storage;

	/**
	 * List of Tasks managed
	 */
	private List<Task> _tasks;

	/**
	 * The list of search result from the latest search. This is used for
	 * confirm command handler
	 */
	private List<Task> _previousSearchResult = null;

	private Task _beforeUpdate = null; // To facilitate undo for Update action

	private static final String FILENAME = "task_record.txt";
	private static final String FILE_ERROR = "Error importing data";

	/**
	 * Constructor which imports all the tasks from a text file when user opens
	 * the application
	 */
	public Database() {
		try {
			importFromFile();
		} catch (FileNotFoundException e) {
			_tasks = new ArrayList<Task>();
			try {
				createNewFile(FILENAME);
			} catch (IOException e2) {
				MainConsole.getInstance().writeMessage(FILE_ERROR);
			}
		} catch (IOException e) {
			MainConsole.getInstance().writeMessage(FILE_ERROR);
		}
		_storage = this;
	}

	public static Database getInstance() {
		if (_storage == null) {
			_storage = new Database();
		}
		return _storage;
	}

	/**
	 * Assign a special taskId to the new task, then add the Task into the
	 * database <br/>
	 * 
	 * @param task
	 * @return true if the task is successfully added <br/>
	 *         false if there is a duplicated task in the list
	 */
	public boolean createTask(Task task) {
		if (isTaskExist(task)) {
			return false;
		} else {
			_tasks.add(task);
			saveToFile();
			return true;
		}
	}

	/**
	 * Update an existing Task <br/>
	 * 
	 * @param taskId
	 *            the id of the existing task newTask the replacing task object
	 * @return true if the taskId is found <br/>
	 *         false if the taskId is not found in the list
	 */
	public boolean updateTask(int taskId, Task newTask) {
		Task task = getTaskWithId(taskId);

		if (task != null) {
			_tasks.remove(task);
			_tasks.add(newTask);
			_beforeUpdate = task;
			saveToFile();

			return true;
		} else {
			return false;
		}
	}

	public Task getBeforeUpdate() {
		return _beforeUpdate;
	}

	public void setBeforeUpdateNull() {
		_beforeUpdate = null;
	}

	/**
	 * Delete an existing Task <br/>
	 * 
	 * @param taskId
	 *            the taskId field of the targeting task
	 * @return true if the taskId is found and the task is successfully deleted <br/>
	 *         false if there is no such task with the taskId in the list
	 */
	public boolean deleteTask(int taskId) {
		Task task = getTaskWithId(taskId);

		if (task != null) {
			_tasks.remove(task);
			saveToFile();
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Sort the Tasks by the Attribute in ascending/descending order according
	 * to the isAcs <br/>
	 * 
	 * @param attr
	 *            main the attributeType to be sorted
	 * @param isAsc
	 *            will the Tasks be ordered ascendingly
	 */
	public List<Task> orderTask(AttributeType attr, boolean isAsc) {
		switch (attr) {
		case endingTime:
			Collections.sort(_tasks, new Task.ByDate());
			break;
		default:
		}
		if (!isAsc) {
			Collections.reverse(_tasks);
		}
		return _tasks;
	}

	/**
	 * Display the Tasks in the database with the status sent<br/>
	 * 
	 * @param status
	 *            to filter
	 * 
	 * @return the list of Tasks
	 */
	public List<Task> displayTask(TaskStatus status) {
		List<Task> allTasks = orderTask(AttributeType.endingTime, true);

		List<Task> returnTasks = new ArrayList<Task>();
		for (Task task : allTasks) {
			if (status == TaskStatus.invalid || task.getStatus() == status) {
				returnTasks.add(task);
			}
		}

		return returnTasks;
	}

	/**
	 * Search for the relevant Tasks. <br/>
	 * Relevant Tasks must have the searching contents in the its name or
	 * description <br/>
	 * 
	 * @param keys
	 *            the strings to be searched
	 * @param isAsc
	 *            the order of results returned in ascending or descending
	 *            endingTimes
	 * @return the list of relevant Tasks in order of endingTime
	 */
	public List<Task> searchTask(boolean isAsc, String... keys) {
		List<Task> tasksByName = searchTask(AttributeType.name, isAsc, keys);

		if (tasksByName == null) {
			tasksByName = new ArrayList<Task>();
		}

		return tasksByName;
	}

	/**
	 * Search for the relevant Tasks. <br/>
	 * Relevant Tasks must have the searching contents in the field specified by
	 * attr <br/>
	 * 
	 * @param attr
	 *            the attribute to be searched
	 * @param isAsc
	 *            the order of results returned in ascending or descending
	 *            endingTimes
	 * @param keys
	 *            the strings to be searched
	 * @return the list of relevant Tasks in order of endingTime, null if cannot
	 *         find
	 */
	public List<Task> searchTask(AttributeType attr, boolean isAsc, String... keys) {
		List<Task> results = new ArrayList<Task>();
		TreeMap<Integer, ArrayList<Task>> relevantTasks = new TreeMap<Integer, ArrayList<Task>>();

		int numKeysContained;

		for (Task task : _tasks) {
			checkOverdue(task);
			numKeysContained = 0;
			for (String key : keys) {
				if (attributeSearch(attr, task, key)) {
					numKeysContained++;
				}
			}
			if (numKeysContained > 0) {
				if (!relevantTasks.containsKey(numKeysContained)) {
					relevantTasks.put(numKeysContained, new ArrayList<Task>());
				}
				relevantTasks.get(numKeysContained).add(task);
			}
		}
		while (!relevantTasks.isEmpty()) {
			List<Task> containSameNumKeys = relevantTasks.pollLastEntry().getValue();
			Collections.sort(containSameNumKeys, new Task.ByDate());
			if (!isAsc) {
				Collections.reverse(containSameNumKeys);
			}
			results.addAll(containSameNumKeys);
		}

		return results;
	}

	/**
	 * Check if the ending time of a task is before the current time
	 * 
	 * @param task
	 *            The task to be checked
	 */
	private void checkOverdue(Task task) {
		TaskStatus status = task.getStatus();
		DateHelper dh = new DateHelper();
		if (status == TaskStatus.undone && dh.compareToCurrentDateTime(task.getEndingTime()) == -1) {
			task.setStatus(TaskStatus.overdue);
		}
	}

	/**
	 * Determine if a task contains the key in the given attribute type
	 * 
	 * @param attr
	 *            specified attribute type
	 * @param task
	 * @param key
	 * @return true if the key is found
	 */
	private boolean attributeSearch(AttributeType attr, Task task, String key) {
		boolean isContainKey = true;
		switch (attr) {
		case name:
			if (!task.getName().toLowerCase().contains(key.toLowerCase())) {
				isContainKey = false;
			}
			break;
		case type:
			if (!task.getTaskType().equals(key)) {
				isContainKey = false;
			}
			break;
		case description:
			if (!task.getDescription().toLowerCase().contains(key.toLowerCase())) {
				isContainKey = false;
			}
			break;
		case startingTime:
			DateTime startingTime = task.getStartingTime();
			if (startingTime == null || !startingTime.toString().contains(key)) {
				isContainKey = false;
			}
			break;
		case endingTime:
			DateTime endingTime = task.getEndingTime();
			if (endingTime == null || !endingTime.toString().contains(key)) {
				isContainKey = false;
			}
			break;

		case status:
			if (!task.getStatus().equals(TaskStatus.valueOf(key))) {
				isContainKey = false;
			}
			break;
		default:
			isContainKey = false;
		}
		return isContainKey;
	}

	/**
	 * Check for the existing task <br/>
	 * This is used to check for duplicated Tasks <br/>
	 * Duplicating tasks = Tasks with the same name, type, and starting & ending
	 * time if any <br/>
	 * 
	 * @param target
	 *            to be compared
	 * @return true if duplicated task is found in the list <br/>
	 *         false if no duplicated found
	 */
	public boolean isTaskExist(Task target) {
		for (Task task : _tasks) {
			if (target.compareTo(task) == 0) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Return the list of previous search result <br/>
	 * 
	 * @return list of previous search result <br/>
	 */
	public List<Task> getPreviousSearchResult() {
		return _previousSearchResult;
	}

	public void setPreviousSearchResult(List<Task> results) {
		_previousSearchResult = results;
	}

	/**
	 * Check the newTask whether it collides with any Task in the Storage
	 * 
	 * @param newTask
	 *            to be checked
	 * @return true if there exists a task collided with newTask
	 */
	public boolean isCollideWithExistingTask(Task newTask) {
		for (Task task : _tasks) {
			if (Task.checkCollision(task, newTask)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Free free slots with length at least equal to the duration specified,
	 * within a specified time frame <br/>
	 * 
	 * @param duration
	 *            the minimun length of the free slots, in minutes
	 * @param startingTime
	 *            the starting time of the time frame
	 * @param endindTime
	 *            the ending time of the time frame
	 * 
	 * @return the list of "dummy timed Tasks", each have a startingTime and
	 *         endingTime set to the free slot
	 */

	//@author A0088498A
	public List<Task> findFreeSlot(int duration, DateTime startingTime, DateTime endingTime) {
		/**
		 * Internal structure holding DateTime with a boolean value indicating whether that DateTime is a starting Time of a Task
		 */
		class DateTimeTuple implements Comparable<DateTimeTuple>{
			public DateTime dateTime;
			public boolean isStartingTime;
			DateTimeTuple (DateTime dateTime, boolean isStartingTime) {
				this.dateTime = dateTime;
				this.isStartingTime = isStartingTime;
				
			}
			/**
			 * Compare 2 DateTimeTuple based on comparison of theirs dateTimes <br/>
			 * In case of Draw, isStartingTime will be used to tie-break based on the rules: x = x, true < false. <br/> 
			 */
			public int compareTo(DateTimeTuple other) {
				int dateTimeComparison = dateTime.compareTo(other.dateTime);
				if (dateTimeComparison == 0) {
					if (isStartingTime == other.isStartingTime) {
						return 0;
					} else if (isStartingTime) {
						return -1;
					} else {
						return 1;
					}
				} else {
					return dateTimeComparison;
				}
			}
		}
		
		List<DateTimeTuple> timeList = new ArrayList<DateTimeTuple>();
		List<Task> freeSlots = new ArrayList<Task>();

		// Add all DateTime in Timed Task into the timeList (and mark the startingTime/endingTime)
		for (Task task : _tasks) {
			if (task.getTaskType() == TaskType.timed) {
				DateTimeTuple taskStartingTime = new DateTimeTuple(task.getStartingTime(), true);
				DateTimeTuple taskEndingTime = new DateTimeTuple(task.getEndingTime(), false);
				timeList.add(taskStartingTime);
				timeList.add(taskEndingTime);
			}
		}
		
		// Treat startingTime and endingTime two instant timed tasks and add to the timeList
		DateTimeTuple rangeStartingTime_Begin = new DateTimeTuple(startingTime, true);
		DateTimeTuple rangeStartingTime_End = new DateTimeTuple(startingTime, false);
		DateTimeTuple rangeEndingTime_Begin = new DateTimeTuple(endingTime, true);
		DateTimeTuple rangeEndingTime_End = new DateTimeTuple(endingTime, false);
		timeList.add(rangeStartingTime_Begin);
		timeList.add(rangeStartingTime_End);
		timeList.add(rangeEndingTime_Begin);
		timeList.add(rangeEndingTime_End);
		
		// Sort
		Collections.sort(timeList);
		
		int count = 0; // number of startingTime without appropriate endingTime
		for (int i=0; i<timeList.size(); i++) {
			DateTimeTuple t = timeList.get(i);
			DateTimeTuple previous = null;
			if (i > 0) {
				previous = timeList.get(i-1);
			}
			
			if (t.isStartingTime) {
				count++;
				
				// the first startingTime without endingTime? then the previous time slot must be free
				if (count == 1 && previous != null) {
					Task freeSlot = new Task ("", TaskType.timed, "", previous.dateTime, t.dateTime);
					
					// Check for requirements: between the from..to.. range? duration enough?
					if (freeSlot.getDuration() >= duration && freeSlot.getStartingTime().compareTo(startingTime) >= 0 && freeSlot.getEndingTime().compareTo(endingTime) <= 0) {
						freeSlots.add(freeSlot);
					}
				}
			} else {
				count--;
			}
		}

		return freeSlots;
	}

	/**
	 * Return the task with taskId in the list
	 * 
	 * @param taskId
	 *            the taskId of the searching task
	 * @return Task in the list with the taskId provided if found <br/>
	 *         null if no Task in the list with such taskId
	 */
	private Task getTaskWithId(int taskId) {
		for (Task task : _tasks) {
			if (task.getTaskId() == taskId) {
				return task;
			}
		}
		return null;
	}

	//@author A0100965J
	/**
	 * Read Tasks stored from file to local memory <br/>
	 * 
	 * @throws FileNotfoundException
	 *             when the file cannot be found
	 * @throws IOException
	 *             when cannot read from the file
	 */
	private void importFromFile() throws FileNotFoundException, IOException {
		_tasks = new ArrayList<Task>();

		File storageFile = new File(FILENAME);
		BufferedReader br;

		br = new BufferedReader(new InputStreamReader(new FileInputStream(storageFile)));
		String line;
		while ((line = br.readLine()) != null) {
			Task task = stringToTask(line);
			_tasks.add(task);
		}
		br.close();
	}

	/**
	 * Convert the raw form of task (in the form
	 * name|type|description|startingTime|endingTime|deadline|) into a Task
	 * object
	 * 
	 * @param line
	 *            the raw form of task in the file
	 * @return the Task object with all parsed information
	 */
	private Task stringToTask(String line) {
		Task task = null;

		assert (!line.equals(""));

		String[] attributes = line.split("\\|");
		for (int i = 0; i < attributes.length; i++) {
			attributes[i] = attributes[i].trim();
		}
		String name = attributes[0];
		String description = attributes[2];

		TaskType type;
		try {
			type = TaskType.valueOf(attributes[1]);
		} catch (Exception e) {
			type = TaskType.invalid;
		}
		// SimpleDateFormat df = new SimpleDateFormat("dd-MM-yy");

		try {
			switch (type) {
			case timed:
				DateTime startingTime = new DateTime(attributes[3]);
				DateTime endingTime = new DateTime(attributes[4]);
				task = new Task(name, type, description, startingTime, endingTime);
				break;
			case deadline:
				DateTime deadline = new DateTime(attributes[4]);
				task = new Task(name, type, description, null, deadline);
				break;
			case floating:
				task = new Task(name, type, description, null, null);
				break;
			case invalid:
				break;
			default:
				break;
			}
		} catch (Exception e) {
			e.printStackTrace();
			// Report parsing error
		}

		if (attributes[5].compareTo("undone") != 0) {
			TaskStatus status = TaskStatus.valueOf(attributes[5]);
			task.setStatus(status);
		}
		return task;
	}

	/**
	 * Create new file
	 * 
	 * @param fileName
	 *            the name of the file to create
	 * @return file File object of an created file
	 * @throws IOExceiption
	 *             when the file cannot be created
	 */
	private File createNewFile(String fileName) throws IOException {
		File file = new File(fileName);
		file.createNewFile();
		return file;
	}

	/**
	 * Save the local on-memory database to file <br/>
	 */
	private void saveToFile() {
		try {
			// Initialise the writer
			File file = new File(FILENAME);
			FileOutputStream fos = new FileOutputStream(file, false); // false =
																		// not
																		// appending
			OutputStreamWriter osw = new OutputStreamWriter(fos);
			BufferedWriter bw = new BufferedWriter(osw);

			// Write task to file
			for (Task task : _tasks) {
				bw.write(task.toString());
				bw.write("\r\n");
			}

			// Close writer and stream
			bw.close();
			osw.close();
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
			// Cannot open or write to file
		}
	}
}